package com.weimob.mcloud.wechatwork.customer.config;

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * Redis配置（配置参数从apollo拉取）
 *
 * @author Easley
 * @since 1.0
 */
@Data
@Configuration
public class RedisConfig {

    /**
     * redis使用的哪个数据库（默认是0）
     */
    @Value("${redis.database}")
    private Integer database;

    /**
     * redis的域名/ip
     */
    @Value("${redis.host}")
    private String host;

    /**
     * 连接redis的密码
     */
    @Value("${redis.password}")
    private String password;

    /**
     * 连接redis的端口
     */
    @Value("${redis.port}")
    private Integer port;

    /**
     * redis连接超时时间
     */
    @Value("${redis.timeout}")
    private Integer timeout;

    /**
     * 连接池建立连接最大等待时间(ms)
     */
    @Value("${redis.pool.maxWait}")
    private Long poolMaxWait;

    /**
     * 连接池最大连接数
     */
    @Value("${redis.pool.maxActive}")
    private Integer poolMaxActive;

    /**
     * 连接池最大等待连接中的数量
     */
    @Value("${redis.pool.maxIdle}")
    private Integer poolMaxIdle;

    /**
     * 连接池最小等待连接中的数量
     */
    @Value("${redis.pool.minIdle}")
    private Integer poolMinIdle;

    /**
     * Jedis连接工厂
     *
     * @author Easley
     * @since 1.0
     */
    @Bean
    public RedisConnectionFactory jedisConnectionFactory() {
        // 1.Redis单机配置，若后续改成集群需要更改配置
        RedisStandaloneConfiguration redisStandaloneConfig = new RedisStandaloneConfiguration();
        redisStandaloneConfig.setDatabase(database);
        redisStandaloneConfig.setHostName(host);
        redisStandaloneConfig.setPort(port);
        redisStandaloneConfig.setPassword(password);

        // 2.Jedis连接池配置
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxWaitMillis(poolMaxWait);
        poolConfig.setMaxTotal(poolMaxActive);
        poolConfig.setMaxIdle(poolMaxIdle);
        poolConfig.setMinIdle(poolMinIdle);
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setTestOnBorrow(true);
        poolConfig.setTestOnCreate(true);
        poolConfig.setTestWhileIdle(true);

        // 3.Jedis客户端配置：超时时间+连接池配置
        JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder();
        builder.connectTimeout(Duration.ofMillis(timeout));
        builder.usePooling().poolConfig(poolConfig);

        // 4.使用Redis单机配置+Jedis客户端配置创建Jedis连接工厂
        return new JedisConnectionFactory(redisStandaloneConfig, builder.build());
    }

    /**
     * 默认RedisTemplate
     * key类型：String
     * value类型：Object
     *
     * @author Easley
     * @since 1.0
     */
    @Bean
    public RedisTemplate redisTemplate() {
        // 1.初始化默认的RedisTemplate，使用 String-Object的 KV类型来增加适用性
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory());

        // 2.配置普通key和Hash结构Key的序列化器，这里都使用StringRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        // 3.配置Value和Hash结构Value的序列化器，这里都使用GenericJackson2JsonRedisSerializer来支持泛型的序列化
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);

        return redisTemplate;
    }

    /**
     * String-String RedisTemplate
     * key类型：String
     * value类型：String
     *
     * @author Easley
     * @since 1.0
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        // 1.初始化StringRedisTemplate
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        // 2.配置Jedis连接工厂
        stringRedisTemplate.setConnectionFactory(jedisConnectionFactory());

        return stringRedisTemplate;
    }

    /**
     * 让SpringIOC维护一个Redis缓存管理器
     * 注意：CacheManager只对使用了SpringCache注解的地方生效，例如：
     * - {@link org.springframework.cache.annotation.Cacheable}
     * - {@link org.springframework.cache.annotation.CachePut}
     * - {@link org.springframework.cache.annotation.CacheEvict}
     * 因此它并不影响RedisTemplate的序列化、反序列化方式。
     * {@link GenericJackson2JsonRedisSerializer}将对象转json时会增加对象类型的属性，反序列化可以直接取出强转，例如：
     * - {@code List<User> users = (List<User>)redisTemplate.opsForValue().get("users");}
     *
     * @return Redis的缓存管理器
     * @author Easley
     * @since 1.0
     */
    @Bean
    public CacheManager cacheManager() {
        // 1.默认配置：spring cache缓存默认有效期1小时
        //   如果没在initialCacheConfig中配置某个key特定的过期时间，就用默认过期时间
        RedisCacheConfiguration defaultCacheConfig = buildStringObjectRedisCacheConfiguration(
                Duration.ofHours(1), false);

        // 2.初始化配置：针对不同cacheName，设置不同的过期时间
        Map<String, RedisCacheConfiguration> initialCacheConfig = new HashMap<>();
        //   I.用于测试spring cache可用性的key，与业务无关。
        //     - 其实此处可以用jdk默认的序列化，但是我懒得多写一个方法，测试key的性能影响可忽略不计
        //     - 其次担心代码被随便copy，导致某个key的value泛型反序列化失败
        initialCacheConfig.put("test:spring:cache:demo"
                , buildStringObjectRedisCacheConfiguration(Duration.ofMinutes(5), true));
        //   II.业务相关key的超时配置在此处加上

        // 3.配置默认配置Jedis连接工厂、默认配置、初始化配置
        return RedisCacheManager.builder(RedisCacheWriter.lockingRedisCacheWriter(jedisConnectionFactory()))
                .cacheDefaults(defaultCacheConfig)
                .withInitialCacheConfigurations(initialCacheConfig)
                .transactionAware()
                .build();
    }

    /**
     * 构造String-Object类型KV的RedisCache配置
     * - key的默认序列化：StringRedisSerializer
     * - value的默认序列化：GenericJackson2JsonRedisSerializer（支持泛型的反序列化）
     *
     * @param entryTtl key超时时间
     * @param disableCachingNullValues 配置是否禁止 redisCache.put(key, null)的操作，如果存null就会抛异常
     * @return String-Object类型KV的RedisCache配置
     * @author Easley
     * @since 1.0
     */
    private static RedisCacheConfiguration buildStringObjectRedisCacheConfiguration(
            Duration entryTtl, boolean disableCachingNullValues) {

        return buildRedisCacheConfiguration(
                // 1.配置缓存失效时间
                entryTtl
                // 2.配置key的序列化方式使用 StringRedisSerializer，String转成了byte[]/null
                , RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
                // 3.配置value的序列化方式使用 GenericJackson2JsonRedisSerializer（支持泛型的反序列化）
                , RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
                // 4.配置是否禁止 redisCache.put(key, null)的操作，如果存null就会抛异常
                , disableCachingNullValues);
    }

    /**
     * 构造RedisCache的配置
     *
     * @param entryTtl key超时时间
     * @param keySerializationPair key的序列化配置
     * @param valueSerializationPair  value的序列化配置
     * @param disableCachingNullValues 配置是否禁止 redisCache.put(key, null)的操作，如果存null就会抛异常
     * @return RedisCache的配置
     * @author Easley
     * @since 1.0
     */
    private static RedisCacheConfiguration buildRedisCacheConfiguration(
            Duration entryTtl
            , RedisSerializationContext.SerializationPair<String> keySerializationPair
            , RedisSerializationContext.SerializationPair<?> valueSerializationPair
            , boolean disableCachingNullValues) {

        // 1.初始化RedisCacheConfiguration，配置失效时间
        //   warn: 注意，RedisCacheConfiguration的每个参数设置方法，都返回了一个new出来的新config，
        //         别直接set了参数就拿之前的config返回了，要注意通过：config = config.xxx()来更新config的引用
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(entryTtl);

        // 2.配置key的序列化方式
        if (keySerializationPair != null) {
            config = config.serializeKeysWith(keySerializationPair);
        }
        // 3.配置value的序列化方式
        if (valueSerializationPair != null) {
            config = config.serializeValuesWith(valueSerializationPair);
        }
        // 4.配置是否禁止 redisCache.put(key, null)的操作，如果存null就会抛异常
        if (disableCachingNullValues) {
            config = config.disableCachingNullValues();
        }

        return config;
    }

    @Bean
    public RedissonClient getRedisson(){
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer()
                .setAddress(host + ":" + port)
                .setDatabase(database)
                .setTimeout(timeout);
        if (null != password) {
            singleServerConfig.setPassword(password);
        }
        return Redisson.create(config);
    }
}
